/*
 *    Copyright (c) 2018-2025, lengleng All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * Neither the name of the pig4cloud.com developer nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * Author: lengleng (wangiegie@gmail.com)
 */
package com.pig4cloud.pigx.user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.customize.api.enums.AbnormalEnum;
import com.pig4cloud.pigx.common.customize.api.exception.OpenApiException;
import com.pig4cloud.pigx.common.customize.api.util.OpenApiUtil;
import com.pig4cloud.pigx.common.customize.enums.CommonDictEnum;
import com.pig4cloud.pigx.common.customize.i18n.util.MessageUtils;
import com.pig4cloud.pigx.common.customize.mysql.utils.MysqlUtil;
import com.pig4cloud.pigx.common.customize.security.utils.SecurityUtil;
import com.pig4cloud.pigx.common.customize.utils.CommonUtil;
import com.pig4cloud.pigx.common.customize.utils.id.IdUtils;
import com.pig4cloud.pigx.common.security.service.PigxUser;
import com.pig4cloud.pigx.system.util.SendEmailUtil;
import com.pig4cloud.pigx.user.constant.I18nMessages;
import com.pig4cloud.pigx.user.constant.RedisMessageType;
import com.pig4cloud.pigx.user.dto.AppUserApiKeyAddDTO;
import com.pig4cloud.pigx.user.dto.AppUserApiKeyEditDTO;
import com.pig4cloud.pigx.user.dto.UserApiKeyLockDTO;
import com.pig4cloud.pigx.user.entity.User;
import com.pig4cloud.pigx.user.entity.UserApiIp;
import com.pig4cloud.pigx.user.entity.UserApiKey;
import com.pig4cloud.pigx.user.entity.UserApiKeyLockDetail;
import com.pig4cloud.pigx.user.enums.UserApiIpEnum;
import com.pig4cloud.pigx.user.enums.UserApiKeyEnum;
import com.pig4cloud.pigx.user.enums.UserApiKeyLockDetailEnum;
import com.pig4cloud.pigx.user.mapper.UserApiIpMapper;
import com.pig4cloud.pigx.user.mapper.UserApiKeyLockDetailMapper;
import com.pig4cloud.pigx.user.mapper.UserApiKeyMapper;
import com.pig4cloud.pigx.user.mapper.UserMapper;
import com.pig4cloud.pigx.user.mapstruct.UserApiKeyStruct;
import com.pig4cloud.pigx.user.service.UserApiKeyService;
import com.pig4cloud.pigx.user.util.UserUtil;
import com.pig4cloud.pigx.user.vo.AppUserApiKeyAddResVO;
import com.pig4cloud.pigx.user.vo.AppUserApiKeyDetailVO;
import com.pig4cloud.pigx.user.vo.AppUserApiKeyPageVO;
import com.pig4cloud.pigx.user.vo.UserApiConfigVO;
import com.pig4cloud.pigx.user.vo.UserApiIpCacheVO;
import com.pig4cloud.pigx.user.vo.UserApiKeyCacheVO;
import com.pig4cloud.pigx.user.vo.UserApiKeyInfoVO;
import com.pig4cloud.pigx.user.vo.UserApiKeyVO;
import lombok.AllArgsConstructor;
import org.apache.logging.log4j.util.Strings;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 用户apiKey
 *
 * @since 2022-10-28 15:47:24
 */
@Service
@AllArgsConstructor
public class UserApiKeyServiceImpl extends ServiceImpl<UserApiKeyMapper, UserApiKey> implements UserApiKeyService {

    private final UserApiKeyLockDetailMapper userApiKeyLockDetailMapper;
    private final UserApiIpMapper userApiIpMapper;

    private final UserApiKeyMapper userApiKeyMapper;
    private final UserMapper userMapper;

    private final CommonUtil commonUtil;
    private final UserUtil userUtil;
    private final SendEmailUtil sendEmailUtil;
    private final SecurityUtil securityUtil;
    private final RedisTemplate redis;

    /**
     * 分页查询
     *
     * @param page      分页条件
     * @param condition 查询条件
     * @return {@link Page< UserApiKeyVO>}
     * @since 2022/10/28 16:52
     */
    @Override
    public Page<UserApiKeyVO> page(Page<UserApiKey> page, UserApiKey condition) {
        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .eq(!Objects.isNull(condition.getUserId()), UserApiKey::getUserId, condition.getUserId())
                .like(Strings.isNotBlank(condition.getName()), UserApiKey::getName, condition.getName())
                .like(Strings.isNotBlank(condition.getAccessKey()), UserApiKey::getAccessKey, condition.getAccessKey())
                .eq(Strings.isNotBlank(condition.getStatus()), UserApiKey::getStatus, condition.getStatus())
                .eq(UserApiKey::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue())
                .orderByDesc(UserApiKey::getLockCount);

        page = baseMapper.selectPage(page, query);

        // 转换为vo类
        List<UserApiKeyVO> voList = UserApiKeyStruct.INSTANCE.list2VOList(page.getRecords());
        // 遍历，翻译字典项
        voList.forEach(vo -> {
            vo.setStatus(UserApiKeyEnum.Status.findName(vo.getStatus()));

            // 权限
            String permission = vo.getPermission();
            StringBuilder permissionName = new StringBuilder();
            if (Strings.isNotEmpty(permission)) {
                String[] split = permission.split(" ");

                // 翻译并重新组成字符串
                for (int i = 0; i < split.length; i++) {
                    String name = UserApiKeyEnum.Permission.findName(split[i]);
                    permissionName.append(name);

                    if (i < split.length - 1) {
                        permissionName.append(" ");
                    }
                }
            }
            vo.setPermission(permissionName.toString());
        });

        return MysqlUtil.transferPage(page, voList);
    }

    /**
     * 人工封禁
     *
     * @param dto dto
     * @since 2022/10/31 14:34
     */
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void lockManual(UserApiKeyLockDTO dto) {
        // 获取当前用户
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        LocalDateTime now = LocalDateTime.now();

        // 查询并锁定ApiKey
        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .eq(UserApiKey::getId, dto.getId())
                .eq(UserApiKey::getStatus, UserApiKeyEnum.Status.NORMAL.getValue())
                .last("for update");
        UserApiKey apiKey = baseMapper.selectOne(query);
        if (Objects.isNull(apiKey)) {
            throw new RuntimeException("ApiKey状态已发生改变");
        }

        // 初始化封禁记录信息
        UserApiKeyLockDetail detail = new UserApiKeyLockDetail();
        detail.setApiKeyId(dto.getId());
        detail.setType(UserApiKeyLockDetailEnum.Type.LOCK.getValue());
        detail.setRemark(dto.getRemark());
        detail.setCreateName(user.getName());
        securityUtil.initCreateAbout(detail, user, now);

        // 修改ApiKey信息
        UpdateWrapper<UserApiKey> updateApiKey = new UpdateWrapper<>();
        updateApiKey.lambda()
                .set(UserApiKey::getLockId, detail.getId())
                .setSql("lock_count = lock_count + 1")
                .set(UserApiKey::getUpdateTime, now)
                .set(UserApiKey::getUpdateBy, user.getId())
                .set(UserApiKey::getStatus, UserApiKeyEnum.Status.LOCK.getValue())
                .eq(UserApiKey::getId, dto.getId());
        if (baseMapper.update(null, updateApiKey) != 1) {
            throw new RuntimeException("操作失败");
        }

        // 持久化封禁记录
        if (userApiKeyLockDetailMapper.insert(detail) != 1) {
            throw new RuntimeException("操作失败");
        }

        // 更新ApiKey缓存
        userUtil.flushRedisApiAccessKey(null, dto.getId());
    }

    /**
     * 人工解封
     *
     * @param dto dto
     * @since 2022/10/31 14:34
     */
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void unlockManual(UserApiKeyLockDTO dto) {
        // 获取当前用户
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        LocalDateTime now = LocalDateTime.now();

        // 查询并锁定ApiKey
        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .eq(UserApiKey::getId, dto.getId())
                .eq(UserApiKey::getStatus, UserApiKeyEnum.Status.LOCK.getValue())
                .last("for update");
        UserApiKey apiKey = baseMapper.selectOne(query);
        if (Objects.isNull(apiKey)) {
            throw new RuntimeException("ApiKey状态已发生改变");
        }

        // 初始化解封记录信息
        UserApiKeyLockDetail detail = new UserApiKeyLockDetail();
        detail.setApiKeyId(dto.getId());
        detail.setType(UserApiKeyLockDetailEnum.Type.UNLOCK.getValue());
        detail.setLockId(apiKey.getLockId());
        detail.setRemark(dto.getRemark());
        detail.setCreateName(user.getName());
        securityUtil.initCreateAbout(detail, user, now);

        // 修改ApiKey信息
        UpdateWrapper<UserApiKey> updateApiKey = new UpdateWrapper<>();
        updateApiKey.lambda()
                .setSql("unlock_count = unlock_count + 1")
                .set(UserApiKey::getLockId, null)
                .set(UserApiKey::getUpdateTime, now)
                .set(UserApiKey::getUpdateBy, user.getId())
                .set(UserApiKey::getStatus, UserApiKeyEnum.Status.NORMAL.getValue())
                .eq(UserApiKey::getId, dto.getId());
        if (baseMapper.update(null, updateApiKey) != 1) {
            throw new RuntimeException("操作失败");
        }

        // 持久化解封记录
        if (userApiKeyLockDetailMapper.insert(detail) != 1) {
            throw new RuntimeException("操作失败");
        }

        // 更新ApiKey缓存
        userUtil.flushRedisApiAccessKey(null, dto.getId());
    }

    /**
     * Dapp-分页查询
     *
     * @param page      分页数据
     * @param condition 查询条件
     * @return {@link Page< AppUserApiKeyPageVO>}
     * @since 2022/11/1 17:10
     */
    @Override
    public Page<AppUserApiKeyPageVO> pageApp(Page<UserApiKey> page, UserApiKey condition) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();

        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .eq(UserApiKey::getUserId, user.getId())
                .eq(UserApiKey::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue())
                .orderByDesc(UserApiKey::getCreateTime);

        page = baseMapper.selectPage(page, query);

        // 转换为vo类
        List<AppUserApiKeyPageVO> voList = UserApiKeyStruct.INSTANCE.list2AppPageVOList(page.getRecords());
        return MysqlUtil.transferPage(page, voList);
    }

    /**
     * Dapp-删除全部
     *
     * @since 2022/11/1 18:18
     */
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void removeAll() {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        LocalDateTime now = LocalDateTime.now();

        // 查询总数
        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .select(UserApiKey::getAccessKey)
                .eq(UserApiKey::getUserId, user.getId())
                .eq(UserApiKey::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue());
        List<UserApiKey> list = baseMapper.selectList(query);
        if (CollectionUtils.isEmpty(list)) {
            return;
        }

        // 逻辑删除
        UserApiKey param = new UserApiKey();
        param.setUserId(user.getId());
        param.setUpdateTime(now);
        param.setUpdateBy(user.getId());
        if (baseMapper.removeAll(param) != list.size()) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
        }

        // 修改用户中ApiKey计数
        UpdateWrapper<User> updateUser = new UpdateWrapper<>();
        updateUser.lambda()
                .setSql("api_key_num = 0")
                .eq(User::getUid, user.getId());
        if (userMapper.update(null, updateUser) != 1) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
        }

        String[] accessKeys = list.stream()
                .map(UserApiKey::getAccessKey)
                .toArray(String[]::new);

        // 根据AccessKey获取redis缓存的key
        List<String> redisKeyList = new ArrayList<>(accessKeys.length);
        for (String accessKey : accessKeys) {
            redisKeyList.add(UserUtil.getApiAccessKeyRedisKey(accessKey));
        }

        // 根据accessKey删除redis中的缓存
        redis.delete(redisKeyList);
    }

    /**
     * Dapp-删除单挑
     *
     * @param id id
     * @since 2022/11/7 11:13
     */
    @Override
    public void remove(Long id) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        LocalDateTime now = LocalDateTime.now();

        // 查询
        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .select(UserApiKey::getAccessKey)
                .eq(UserApiKey::getId, id)
                .eq(UserApiKey::getUserId, user.getId())
                .eq(UserApiKey::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue());
        UserApiKey apiKey = baseMapper.selectOne(query);
        if (Objects.isNull(apiKey)) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.DATA_STATUS_HAS_CHANGED));
        }

        // 逻辑删除
        UpdateWrapper<UserApiKey> updateApiKey = new UpdateWrapper<>();
        updateApiKey.lambda()
                .set(UserApiKey::getUpdateBy, user.getId())
                .set(UserApiKey::getUpdateTime, now)
                .set(UserApiKey::getIsDeleted, null)
                .eq(UserApiKey::getId, id);
        if (baseMapper.update(null, updateApiKey) != 1) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
        }

        // 修改用户中ApiKey计数
        UpdateWrapper<User> updateUser = new UpdateWrapper<>();
        updateUser.lambda()
                .setSql("api_key_num = api_key_num - 1")
                .eq(User::getUid, user.getId());
        if (userMapper.update(null, updateUser) != 1) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
        }

        // 根据accessKey删除redis中的缓存
        redis.delete(UserUtil.getApiAccessKeyRedisKey(apiKey.getAccessKey()));
    }

    /**
     * Dapp-根据id查询详情
     *
     * @param id id
     * @return {@link AppUserApiKeyDetailVO}
     * @since 2022/11/2 10:42
     */
    @Override
    public AppUserApiKeyDetailVO appDetail(Long id) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();

        // 确保是本人的数据
        QueryWrapper<UserApiKey> queryApiKey = new QueryWrapper<>();
        queryApiKey.lambda()
                .eq(UserApiKey::getId, id)
                .eq(UserApiKey::getUserId, user.getId())
                .eq(UserApiKey::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue());

        // 查询详情
        UserApiKey apiKey = baseMapper.selectOne(queryApiKey);
        if (Objects.isNull(apiKey)) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.DATA_STATUS_HAS_CHANGED));
        }

        AppUserApiKeyDetailVO vo = UserApiKeyStruct.INSTANCE.entity2AppDetailVO(apiKey);

        // SecretKey固定返回******
        vo.setSecretKey("******");

        if (apiKey.getIpNum() == 0) {
            vo.setIpList(Collections.emptyList());
        } else {
            // 查询ip
            QueryWrapper<UserApiIp> queryIp = new QueryWrapper<>();
            queryIp.lambda()
                    .select(UserApiIp::getAddress)
                    .eq(UserApiIp::getApiKeyId, id);
            List<UserApiIp> ipList = userApiIpMapper.selectList(queryIp);
            List<String> addressList = ipList.stream()
                    .map(UserApiIp::getAddress)
                    .collect(Collectors.toList());
            vo.setIpList(addressList);
        }

        return vo;
    }

    /**
     * Dapp-新增
     *
     * @param dto dto
     * @return {@link AppUserApiKeyAddResVO}
     * @since 2022/11/2 11:42
     */
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public AppUserApiKeyAddResVO add(AppUserApiKeyAddDTO dto) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        String message = RedisMessageType.CUSTOMER_API_KEY_CREATE;

        // 校验验证码
        sendEmailUtil.checkVerifyCode(user.getEmail(), message, dto.getVerifyCode());
        // 获取api配置
        UserApiConfigVO apiConfig = userUtil.getApiConfigCache();
        if (Objects.isNull(apiConfig.getMaxApiKeyNumPerUser())) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.SERVICE_ABNORMAL));
        }

        // 增加ApiKey计数
        UpdateWrapper<User> updateUser = new UpdateWrapper<>();
        updateUser.lambda()
                .setSql("api_key_num = api_key_num + 1")
                .eq(User::getUid, user.getId())
                .apply("api_key_num + 1 <= " + apiConfig.getMaxApiKeyNumPerUser());
        if (userMapper.update(null, updateUser) != 1) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.API_KEY_NUM_UPPER_LIMIT, apiConfig.getMaxApiKeyNumPerUser()));
        }

        // 新增ApiKey
        UserApiKey apiKey = new UserApiKey();
        apiKey.setUserId(user.getId());
        apiKey.setName(dto.getName());
        apiKey.setAccessKey(IdUtils.fastSimpleUUID());
        apiKey.setSecretKey(IdUtils.fastSimpleUUID());
        // 默认赋予read读取、transfer划转、contract合约三种权限
        apiKey.setPermission(String.format("%s %s %s", UserApiKeyEnum.Permission.READ.getValue(), UserApiKeyEnum.Permission.TRANSFER.getValue(), UserApiKeyEnum.Permission.CONTRACT.getValue()));
        apiKey.setHasIpLimit(CommonDictEnum.YesOrNo.NO.getValue());
        apiKey.setIpNum(0);
        apiKey.setLockCount(0);
        apiKey.setUnlockCount(0);
        apiKey.setStatus(UserApiKeyEnum.Status.NORMAL.getValue());
        securityUtil.initCreateAbout(apiKey);

        if (baseMapper.insert(apiKey) != 1) {
            throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
        }

        // 组装返回参数，只有新建时能够看到secretKey
        AppUserApiKeyAddResVO res = new AppUserApiKeyAddResVO();
        res.setAccessKey(apiKey.getAccessKey());
        res.setSecretKey(apiKey.getSecretKey());
        return res;
    }

    /**
     * 创建ApiKey时获取邮箱验证码
     *
     * @param time 客户端当前时间字符串
     * @param name 标签
     * @since 2022/11/1 19:52
     */
    @Override
    public void verifyCodeWhenCreate(HttpServletRequest request, String time, String name) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        String email = user.getEmail();
        String message = RedisMessageType.CUSTOMER_API_KEY_CREATE;

        String verifyCode = sendEmailUtil.getVerifyCode(email, message, 60 * 3);

        // 标题内容替换参数
        Map<String, String> titleCode = new HashMap<>(1);
        // 获取ip
        String ip = getClientIp(request);
        titleCode.put("${ip}", ip);
        titleCode.put("${time}", time);
        // 组装内容替换参数
        Map<String, String> contentReplace = new HashMap<>(1);
        contentReplace.put("${ip}", ip);
        contentReplace.put("${time}", time);
        contentReplace.put("${name}", name);
        contentReplace.put("${code}", verifyCode);

        // 发送邮件
        R<Boolean> r = sendEmailUtil.sendEmail(titleCode, contentReplace, email, message, user.getLanguageCode());
        // 发送失败
        if (!r.getIsSuccess()) {
            // 清除缓存
            sendEmailUtil.clearVerifyCode(sendEmailUtil.getVerifyCodeKey(email, message));
            // 抛出异常
            throw new RuntimeException(r.getMsg());
        }
    }

    /**
     * Dapp-修改
     *
     * @param dto dto
     * @since 2022/11/3 11:18
     */
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void edit(AppUserApiKeyEditDTO dto) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        LocalDateTime now = LocalDateTime.now();
        if (CollectionUtils.isEmpty(dto.getIpList())) {
            dto.setIpList(Collections.emptyList());
        }

        // 检查权限
        validPermission(dto, user);
        // 检查IpList
        validIpList(dto);

        // 修改ApiKey设置
        UpdateWrapper<UserApiKey> updateApiKey = new UpdateWrapper<>();
        updateApiKey.lambda()
                .set(UserApiKey::getPermission, dto.getPermission())
                .set(UserApiKey::getHasIpLimit, dto.getHasIpLimit())
                .set(UserApiKey::getIpNum, dto.getIpList().size())
                .set(UserApiKey::getUpdateBy, user.getId())
                .set(UserApiKey::getUpdateTime, now)
                .eq(UserApiKey::getId, dto.getId())
                .eq(UserApiKey::getUserId, user.getId());
        if (baseMapper.update(null, updateApiKey) != 1) {
            // 操作失败
            throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
        }

        // 处理IpList
        handleIpList(dto, user, now);

        // 更新ApiKey缓存
        UserApiKeyCacheVO apiKey = userUtil.flushRedisApiAccessKey(null, dto.getId());

        // 更新ApiIp缓存
        userUtil.flushRedisApiIp(dto.getId(), apiKey.getAccessKey());
    }

    /**
     * 处理IpList
     *
     * @param dto  dto
     * @param user 用户
     * @param now  当前时间
     * @since 2022/11/3 16:16
     */
    private void handleIpList(AppUserApiKeyEditDTO dto, PigxUser user, LocalDateTime now) {
        // 如果设置了不限制IP，不对IP进行任何处理
        if (CommonDictEnum.YesOrNo.NO.getValue().equals(dto.getHasIpLimit())) {
            return;
        }

        // 查询当前的ip配置
        QueryWrapper<UserApiIp> queryOldIp = new QueryWrapper<>();
        queryOldIp.lambda()
                .select(UserApiIp::getAddress)
                .eq(UserApiIp::getApiKeyId, dto.getId())
                .eq(UserApiIp::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue());
        List<UserApiIp> oldApiIpList = userApiIpMapper.selectList(queryOldIp);
        List<String> oldIpList = oldApiIpList.stream()
                .map(UserApiIp::getAddress)
                .collect(Collectors.toList());

        // 取出已经被删除的ip地址
        List<String> removeIpList = new ArrayList<>();
        oldIpList.forEach(oldIp -> {
            if (!dto.getIpList().contains(oldIp)) {
                removeIpList.add(oldIp);
            }
        });

        // 有需要逻辑删除的ip
        if (!CollectionUtils.isEmpty(removeIpList)) {
            UpdateWrapper<UserApiIp> removeIp = new UpdateWrapper<>();
            removeIp.lambda()
                    .set(UserApiIp::getUpdateBy, user.getId())
                    .set(UserApiIp::getUpdateTime, now)
                    .set(UserApiIp::getIsDeleted, CommonDictEnum.IsDeleted.YES.getValue())
                    .eq(UserApiIp::getApiKeyId, dto.getId())
                    .eq(UserApiIp::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue())
                    .in(UserApiIp::getAddress, removeIpList);
            if (userApiIpMapper.update(null, removeIp) != removeIpList.size()) {
                // 操作失败
                throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
            }
        }

        // 取出新增的ip地址
        List<String> newIpList = new ArrayList<>();
        dto.getIpList().forEach(ip -> {
            if (!oldIpList.contains(ip)) {
                newIpList.add(ip);
            }
        });

        if (!CollectionUtils.isEmpty(newIpList)) {
            // 判断新增的ip地址是否曾经使用过
            QueryWrapper<UserApiIp> checkExistIp = new QueryWrapper<>();
            checkExistIp.lambda()
                    .select(UserApiIp::getAddress)
                    .eq(UserApiIp::getApiKeyId, dto.getId())
                    .isNull(UserApiIp::getIsDeleted)
                    .in(UserApiIp::getAddress, newIpList);

            List<UserApiIp> existApiIpList = userApiIpMapper.selectList(checkExistIp);
            List<String> existIpList = existApiIpList.stream()
                    .map(UserApiIp::getAddress)
                    .collect(Collectors.toList());

            // 存在过，修改状态，重置创建时间
            if (!CollectionUtils.isEmpty(existIpList)) {
                UpdateWrapper<UserApiIp> updateExistIp = new UpdateWrapper<>();
                updateExistIp.lambda()
                        .set(UserApiIp::getCreateBy, user.getId())
                        .set(UserApiIp::getCreateTime, now)
                        .set(UserApiIp::getUpdateBy, user.getId())
                        .set(UserApiIp::getUpdateTime, now)
                        .set(UserApiIp::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue())
                        .eq(UserApiIp::getApiKeyId, dto.getId())
                        .isNull(UserApiIp::getIsDeleted)
                        .in(UserApiIp::getAddress, existIpList);
                if (userApiIpMapper.update(null, updateExistIp) != existIpList.size()) {
                    // 操作失败
                    throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
                }
            }

            // 有不曾存在过的ip地址
            if (newIpList.size() > existIpList.size()) {
                // 取出不曾存在过的ip地址
                List<String> actualNewIpList = new ArrayList<>(newIpList.size() - existIpList.size());
                newIpList.forEach(ip -> {
                    if (!existIpList.contains(ip)) {
                        actualNewIpList.add(ip);
                    }
                });

                // 初始化信息
                List<UserApiIp> entityList = new ArrayList<>(actualNewIpList.size());
                actualNewIpList.forEach(ip -> {
                    UserApiIp entity = new UserApiIp();
                    entity.setApiKeyId(dto.getId());
                    entity.setAddress(ip);
                    entity.setLockCount(0);
                    entity.setManualUnlockCount(0);
                    entity.setCurrentLockCount(0);
                    entity.setStatus(UserApiIpEnum.Status.NORMAL.getValue());
                    securityUtil.initCreateAbout(entity);

                    entityList.add(entity);
                });

                // 批量新增
                int partSize = 20;
                for (int i = 0; i < entityList.size(); ) {
                    int endIndex = i + partSize;
                    if (endIndex > entityList.size()) {
                        endIndex = entityList.size();
                    }

                    List<UserApiIp> partList = entityList.subList(i, endIndex);
                    if (userApiIpMapper.insertBatch(partList) != partList.size()) {
                        // 操作失败
                        throw new RuntimeException(MessageUtils.get(I18nMessages.OPERATION_FAILURE));
                    }

                    i = endIndex;
                }
            }
        }
    }

    /**
     * 检查IpList
     *
     * @param dto dto
     * @since 2022/11/3 16:14
     */
    private static void validIpList(AppUserApiKeyEditDTO dto) {
        // 开启了ip白名单
        if (CommonDictEnum.YesOrNo.YES.getValue().equals(dto.getHasIpLimit())) {
            if (CollectionUtils.isEmpty(dto.getIpList())) {
                // ip白名单不能为空
                throw new RuntimeException(MessageUtils.get(I18nMessages.IP_WHITELIST_CANNOT_BE_EMPTY));
            }

            // 遍历
            Map<String, Integer> countMap = new HashMap<>(dto.getIpList().size());
            dto.getIpList().forEach(ip -> {
                // 判断ip地址格式
                CommonUtil.matchIp(ip);
                // 判断是否重复
                countMap.compute(ip, (key, value) -> {
                    if (Objects.isNull(value)) {
                        return 1;
                    } else {
                        throw new RuntimeException(MessageUtils.get(I18nMessages.DUPLICATE_IP, key));
                    }
                });
            });
        }
    }

    /**
     * 检查权限
     *
     * @param dto  dto
     * @param user 用户
     * @since 2022/11/3 16:13
     */
    private void validPermission(AppUserApiKeyEditDTO dto, PigxUser user) {
        // 判断新权限中是否包含了提现
        if (!checkPermissionHasWithdrawal(dto.getPermission())) {
            return;
        }

        // 查询旧记录
        QueryWrapper<UserApiKey> query = new QueryWrapper<>();
        query.lambda()
                .eq(UserApiKey::getId, dto.getId())
                .eq(UserApiKey::getUserId, user.getId())
                .eq(UserApiKey::getIsDeleted, CommonDictEnum.IsDeleted.NO.getValue());
        UserApiKey old = baseMapper.selectById(dto.getId());
        if (Objects.isNull(old)) {
            // 数据状态已发生改变，请刷新重试
            throw new RuntimeException(MessageUtils.get(I18nMessages.DATA_STATUS_HAS_CHANGED));
        }

        // 如果是新开启的提现权限
        if (!checkPermissionHasWithdrawal(old.getPermission())) {
            // 校验邮箱验证码
            sendEmailUtil.checkVerifyCode(user.getEmail(), RedisMessageType.CUSTOMER_API_KEY_ENABLE_WITHDRAWAL, dto.getVerifyCode());
        }

        if (!CommonDictEnum.YesOrNo.YES.getValue().equals(dto.getHasIpLimit())
                || CollectionUtils.isEmpty(dto.getIpList())) {
            // 开启提现权限时，必须设置ip白名单
            throw new RuntimeException(MessageUtils.get(I18nMessages.MUST_SET_IP_WHITELIST));
        }
    }

    /**
     * 判断权限是否包含提现
     *
     * @param permission 权限
     * @return {@link boolean}
     * @since 2022/11/3 16:33
     */
    private static boolean checkPermissionHasWithdrawal(String permission) {
        return Strings.isNotBlank(permission) && permission.contains(UserApiKeyEnum.Permission.WITHDRAWAL.getValue());
    }

    /**
     * ApiKey提现权限修改为允许时获取邮箱验证码
     *
     * @param time 客户端当前时间字符串
     * @param name 标签
     * @since 2022/11/1 20:04
     */
    @Override
    public void verifyCodeWhenModify(String time, String name) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        String email = user.getEmail();
        String message = RedisMessageType.CUSTOMER_API_KEY_ENABLE_WITHDRAWAL;

        String verifyCode = sendEmailUtil.getVerifyCode(email, message, 60 * 3);

        // 组装内容替换参数
        Map<String, String> contentReplace = new HashMap<>(2);
        contentReplace.put("${time}", time);
        contentReplace.put("${name}", name);
        contentReplace.put("${code}", verifyCode);

        // 发送邮件
        R<Boolean> r = sendEmailUtil.sendEmail(null, contentReplace, email, message, user.getLanguageCode());
        // 发送失败
        if (!r.getIsSuccess()) {
            // 清除缓存
            sendEmailUtil.clearVerifyCode(sendEmailUtil.getVerifyCodeKey(email, message));
            // 抛出异常
            throw new RuntimeException(r.getMsg());
        }
    }

    /**
     * 根据AccessKey查询ApiKey所有相关信息
     *
     * @param accessKey 访问密钥
     * @return {@link UserApiKeyInfoVO}
     * @since 2022/11/9 14:35
     */
    @Override
    public UserApiKeyInfoVO getInfo(String accessKey) {
        UserApiKeyCacheVO apiKeyCache = userUtil.getApiKeyCache(accessKey);
        // 为空 || 被封禁
        if (Objects.isNull(apiKeyCache) || !Objects.equals(UserApiKeyEnum.Status.NORMAL.getValue(), apiKeyCache.getStatus())) {
            return null;
        }

        UserApiKeyInfoVO info = UserApiKeyStruct.INSTANCE.cacheVO2InfoVO(apiKeyCache);

        // 查询Ip信息
        List<UserApiIpCacheVO> ipList = userUtil.getApiKeyIpCache(apiKeyCache.getId(), accessKey);
        info.setIpList(ipList);

        return info;
    }

    /**
     * 根据AccessKey查询信息
     *
     * @param accessKey 访问密钥
     * @return {@link UserApiKeyCacheVO}
     * @since 2022/11/9 11:08
     */
    @Override
    public UserApiKeyCacheVO getCache(String accessKey) {
        return userUtil.getApiKeyCache(accessKey);
    }

    /**
     * 根据 accessKey 查询 secretKey
     *
     * @param accessKey accessKey
     * @return r
     */
    @Override
    public R<String> getSecretKey(String accessKey, String verifyCode) {
        try {
            PigxUser user = SecurityUtil.getLoginUserNotNull();
            String message = RedisMessageType.CUSTOMER_SECRET_KEY_SELECT;

            // 校验验证码
            sendEmailUtil.checkVerifyCode(user.getEmail(), message, verifyCode);
            UserApiKey apiKey = userApiKeyMapper.selectOne(new QueryWrapper<UserApiKey>() {{
                eq("user_id", user.getId());
                eq("access_key", accessKey);
                select("secret_key");
            }});
            return R.ok(apiKey.getSecretKey());
        } catch (Exception e) {
            throw OpenApiUtil.getError(AbnormalEnum.MSG.DISCONNECTED);
        }
    }

    /**
     * 用户查询 secretKey 时发送邮件
     *
     * @param time 客户端当前时间字符串
     * @param name 标签
     * @since 2022/11/1 20:04
     */
    @Override
    public R verifyCodeWhenGetSecretKey(String time, String name) {
        PigxUser user = SecurityUtil.getLoginUserNotNull();
        String email = user.getEmail();
        String message = RedisMessageType.CUSTOMER_SECRET_KEY_SELECT;

        String verifyCode = sendEmailUtil.getVerifyCode(email, message, 60 * 3);

        // 组装内容替换参数
        Map<String, String> contentReplace = new HashMap<>(2);
        contentReplace.put("${time}", time);
        contentReplace.put("${name}", name);
        contentReplace.put("${code}", verifyCode);

        // 发送邮件
        R<Boolean> r = sendEmailUtil.sendEmail(null, contentReplace, email, message, user.getLanguageCode());
        // 发送失败
        if (!r.getIsSuccess()) {
            // 清除缓存
            sendEmailUtil.clearVerifyCode(sendEmailUtil.getVerifyCodeKey(email, message));
            // 抛出异常
            throw new RuntimeException(r.getMsg());
        }
        return R.ok();
    }

    /**
     * 从请求头中获取用户Ip地址
     *
     * @param request 请求包
     * @return {@link String}
     * @since 2022/11/8 17:59
     */
    private String getClientIp(HttpServletRequest request) {
        String clientIp = request.getHeader(SecurityConstants.X_CLIENT_IP);
        if (Strings.isBlank(clientIp)) {
            // 无效的IP
            throw new OpenApiException(20004, "Invalid API-key, IP, or permissions for action.");
        }
        String[] split = clientIp.split(SecurityConstants.X_CLIENT_IP_SPLIT);
        return split[0];
    }
}
